{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "# 模型评估和调参"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "前面几章，你学习了分类问题要用到的机器学习算法以及必备的数据预处理算法。现在，是时候学习如何训练一个好用的机器学习模型了，二者要涉及到两个重要的课题：模型评估和参数寻优。\n",
    "\n",
    "本章，我们要学习：\n",
    "* 获得对模型性能的无偏差估计\n",
    "* 诊断常见的机器学习问题\n",
    "* 模型调参\n",
    "* 使用不同的性能评价指标对模型评估"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 通过管道创建工作流\n",
    "\n",
    "当我们应用不同的预处理技巧时，比如对特征标准化、对数据主成分分析，我们都需要重复利用某些参数，比如对训练集标准化后还要对测试集进行标准化(二者必须使用相同的参数)。\n",
    "\n",
    "本节，你会学到一个非常有用的工具：管道(pipeline)，这里的管道不是Linux中的管道，而是sklearn中的Pipeline类，二者做的事情差不多。\n",
    "\n",
    "\n",
    "### 读取Breast Cancer Wisconsin数据集\n",
    "\n",
    "本章，我们要用到一个新的二分类数据集 **Breast Cancer Wisconsin**，它包含569个样本。每一条数据前两列是唯一的ID和相应的类别值(M=恶性肿瘤，B=良性肿瘤)，第3-32列是实数值的特征。\n",
    "\n",
    "话不多说，先读取数据集，然后将y转为0，1："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "df = pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data', header=None)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import LabelEncoder"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "X = df.iloc[:, 2:].values\n",
    "y = df.loc[:, 1].values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array(['B', 'M'], dtype=object)"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "np.unique(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "le = LabelEncoder()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "y = le.fit_transform(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([0, 1])"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "np.unique(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "接着创建训练集和测试集:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "df = pd.read_csv('https')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/57713871846af.png)\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### 将transformer和Estimator放入同一个管道\n",
    "\n",
    "前几章说过，很多机器学习算法要求特征取值范围要相同。因此，我们要对BCW数据集每一列做标准化处理，然后才能应用到线性分类器。此外，我们还想将原始的30维度特征压缩的2维度，这个交给PCA来做。\n",
    "\n",
    "之前我们都是每一步执行一个操作，现在我们学习用管道将 StandardScaler, PCA和LogisticRegression连接起来：\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/57713aa62e474.png)\n",
    "\n",
    "\n",
    "Pipeline对象接收元组构成的列表作为输入，每个元组第一个值作为变量名，元组第二个元素是sklearn中的transformer或Estimator。\n",
    "\n",
    "\n",
    "管道中间每一步由sklearn中的transformer构成，最后一步是一个Estimator。我们的例子中，管道包含两个中间步骤，一个StandardScaler和一个PCA，这俩都是transformer，逻辑斯蒂回归分类器是Estimator。\n",
    "\n",
    "当管道pipe_lr执行fit方法时，首先StandardScaler执行fit和transform方法，然后将转换后的数据输入给PCA，PCA同样执行fit和transform方法，最后将数据输入给LogisticRegression，训练一个LR模型。\n",
    "\n",
    "对于管道来说，中间有多少个transformer都可以。管道的工作方式可以用下图来展示(一定要注意管道执行fit方法，而transformer要执行fit_transform)：\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771db072d42f.png)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    " "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## K折交叉验证评估模型性能\n",
    "\n",
    "训练机器学习模型的关键一步是要评估模型的泛化能力。如果我们训练好模型后，还是用训练集取评估模型的性能，这显然是不符合逻辑的。一个模型如果性能不好，要么是因为模型过于复杂导致过拟合(高方差)，要么是模型过于简单导致导致欠拟合(高偏差)。可是用什么方法评价模型的性能呢？这就是这一节要解决的问题，你会学习到两种交叉验证计数，holdout交叉验证和k折交叉验证， 来评估模型的泛化能力。\n",
    "\n",
    "\n",
    "### holdout method\n",
    "\n",
    "评估模型泛化能力的典型方法是holdout交叉验证(holdout cross validation)。holdout方法很简单，我们只需要将原始数据集分割为训练集和测试集，前者用于训练模型，后者用于评估模型的性能。\n",
    "\n",
    "不过，在训练模型这一步，我们非常关心如何选择参数来提高模型的预测能力，而选择参数这一步被称为模型选择(model selection，译者注：不少资料将选择何种模型算法称为模型选择)，参数选择是非常重要的，因为对于同一种机器学习算法，如果选择不同的参数(超参数)，模型的性能会有很大差别。\n",
    "\n",
    "如果在模型选择的过程中，我们始终用测试集来评价模型性能，这实际上也将测试集变相地转为了训练集，这时候选择的最优模型很可能是过拟合的。\n",
    "\n",
    "\n",
    "更好的holdout方法是将原始训练集分为三部分：训练集、验证集和测试集。训练机用于训练不同的模型，验证集用于模型选择。而测试集由于在训练模型和模型选择这两步都没有用到，对于模型来说是未知数据，因此可以用于评估模型的泛化能力。下图展示了holdout方法的步骤：\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771df1b9b18e.png)\n",
    "\n",
    "当然holdout方法也有明显的缺点，它对数据分割的方式很敏感，如果原始数据集分割不当，这包括训练集、验证集和测试集的样本数比例，以及分割后数据的分布情况是否和原始数据集分布情况相同等等。所以，不同的分割方式可能得到不同的最优模型参数。\n",
    "\n",
    "下一节，我们会学习到一种鲁棒性更好的模型评估方法，k折交叉沿则，即重复k次holdout方法提高鲁棒性。\n",
    "\n",
    "\n",
    "### k折交叉验证\n",
    "\n",
    "k折交叉验证的过程，第一步我们使用不重复抽样将原始数据随机分为k份，第二步 k-1份数据用于模型训练，剩下那一份数据用于测试模型。然后重复第二步k次，我们就得到了k个模型和他的评估结果(译者注：为了减小由于数据分割引入的误差，通常k折交叉验证要随机使用不同的划分方法重复p次，常见的有10次10折交叉验证)。\n",
    "\n",
    "\n",
    "然后我们计算k折交叉验证结果的平均值作为参数/模型的性能评估。使用k折交叉验证来寻找最优参数要比holdout方法更稳定。一旦我们找到最优参数，要使用这组参数在原始数据集上训练模型作为最终的模型。\n",
    "\n",
    "k折交叉验证使用不重复采样，优点是每个样本只会在训练集或测试中出现一次，这样得到的模型评估结果有更低的方法。\n",
    "\n",
    "\n",
    "下图演示了10折交叉验证：\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771e48767a65.png)\n",
    "\n",
    "至于k折中的k到底设定为多少，这个又是一个调参的过程，当然了，这一步很少有人会调参，一般都是用10.但是如果你的数据集特别小，我们当然希望训练集大一点，这时候就要设定大一点的k值，因为k越大，训练集在整个原始训练集的占比就越多。但是呢，k也不能太大，一则是导致训练模型个数过多，二则是k很大的情况下，各个训练集相差不多，导致高方差。要是你的数据集很大，那就把k设定的小一点咯，比如5.\n",
    "\n",
    "\n",
    "\n",
    "其实呢，在将原始数据集划分为k部分的过程中，有很多不同的采样方法，比如针对非平衡数据的分层采样。分层采样就是在每一份子集中都保持原始数据集的类别比例。比如原始数据集正类：负类=3:1，这个比例也要保持在各个子集中才行。sklearn中实现了分层k折交叉验证哦：\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771e77dead41.png)\n",
    "\n",
    "\n",
    "sklearn就是这么方便，此外，更人性化的是sklearn还实现了一个直接得到交叉验证评估结果的方法cross_val_score(内部同样是分层k折交叉验证):\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771e8ef53517.png)\n",
    "\n",
    "\n",
    "\n",
    "cross_val_score方法还支持并行计算。\n",
    "\n",
    "\n",
    "**Note** 交叉验证是如何评估泛化能力的方差，这个问题超出了本书的范围，如果你感兴趣，可以阅读 *Analysis of Variance of Cross-validation Estimators of the Generalized Error. Journal of Machine Learning Research, 6:1127-1168, 2005*\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## 使用学习曲线和验证曲线 调试算法\n",
    "\n",
    "这一节我们学习两个非常有用的诊断方法，可以用来提高算法的表现。他们就是学习曲线(learning curve)和验证曲线(validation curve)。学习曲线可以判断学习算法是否过拟合或者欠拟合。\n",
    "\n",
    "\n",
    "### 使用学习曲线判别偏差和方差问题\n",
    "\n",
    "如果一个模型相对于训练集来说过于复杂，比如参数太多，则模型很可能过拟合。避免过拟合的手段包含增大训练集，但这是不容易做到的。通过画出不同训练集大小对应的训练集和验证集准确率，我们能够很轻松滴检测模型是否方差偏高或偏差过高，以及增大训练集是否有用。\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771f29779896.png)\n",
    "\n",
    "\n",
    "上图的左上角子图中模型偏差很高。它的训练集和验证集准确率都很低，很可能是欠拟合。解决欠拟合的方法就是增加模型参数，比如，构建更多的特征，减小正则项。\n",
    "\n",
    "上图右上角子图中模型方差很高，表现就是训练集和验证集准确率相差太多。解决过拟合的方法有增大训练集或者降低模型复杂度，比如增大正则项，或者通过特征选择减少特征数。\n",
    "\n",
    "\n",
    "这俩问题可以通过验证曲线解决。\n",
    "\n",
    "\n",
    "我们先看看学习曲线是怎么回事吧：\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771f5d6771ea.png)\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/27/5771f5ebea1d6.png)\n",
    "\n",
    "\n",
    "learning_curve中的train_sizes参数控制产生学习曲线的训练样本的绝对/相对数量，此处，我们设置的train_sizes=np.linspace(0.1, 1.0, 10)，将训练集大小划分为10个相等的区间。learning_curve默认使用分层k折交叉验证计算交叉验证的准确率，我们通过cv设置k。\n",
    "\n",
    "上图中可以看到，模型在测试集表现很好，不过训练集和测试集的准确率还是有一段小间隔，可能是模型有点过拟合。\n",
    "\n",
    "\n",
    "\n",
    "### 用验证曲线解决过拟合和欠拟合\n",
    "\n",
    "\n",
    "验证曲线是非常有用的工具，他可以用来提高模型的性能，原因是他能处理过拟合和欠拟合问题。\n",
    "\n",
    "验证曲线和学习曲线很相近，不同的是这里画出的是不同参数下模型的准确率而不是不同训练集大小下的准确率：\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/57726c432448f.png)\n",
    "\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/57726c5e6f593.png)\n",
    "\n",
    "\n",
    "\n",
    "我们得到了参数C的验证曲线。\n",
    "\n",
    "\n",
    "和learning_curve方法很像，validation_curve方法使用采样k折交叉验证来评估模型的性能。在validation_curve内部，我们设定了用来评估的参数，这里是C,也就是LR的正则系数的倒数。\n",
    "\n",
    "\n",
    "观察上图，最好的C值是0.1。\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## 通过网格搜索调参\n",
    "\n",
    "机器学习算法中有两类参数：从训练集中学习到的参数，比如逻辑斯蒂回归中的权重参数，另一类是模型的超参数，也就是需要人工设定的参数，比如正则项系数或者决策树的深度。\n",
    "\n",
    "\n",
    "前一节，我们使用验证曲线来提高模型的性能，实际上就是找最优参数。这一节我们学习另一种常用的超参数寻优算法：网格搜索(grid search)。\n",
    "\n",
    "\n",
    "网格搜索听起来高大上，实际上简单的一笔，就是暴力搜索而已，我们事先为每个参数设定一组值，然后穷举各种参数组合，找到最好的那一组。\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/57726f0da6780.png)\n",
    "\n",
    "\n",
    "GridSearchCV中param_grid参数是字典构成的列表。对于线性SVM，我们只评估参数C；对于RBF核SVM，我们评估C和gamma。\n",
    "\n",
    "最后， 我们通过best\\_parmas_得到最优参数组合。\n",
    "\n",
    "sklearn人性化的一点是，我们可以直接利用最优参数建模(best\\_estimator_)：\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/5772718b8e2bc.png)\n",
    "\n",
    "\n",
    "\n",
    "**Note** 网格搜索虽然不错，但是穷举过于耗时，sklearn中还实现了随机搜索，使用 RandomizedSearchCV类，随机采样出不同的参数组合。\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## 通过嵌套交叉验证选择算法\n",
    "\n",
    "结合k折交叉验证和网格搜索是调参的好手段。可是如果我们想从茫茫算法中选择最合适的算法，用什么方法呢？这就是本节要介绍的嵌套交叉验证(nested cross validation)。 Varma和Simon在论文*Bias in Error Estimation When Using Cross-validation for Model Selection*中指出使用嵌套交叉验证得到的测试集误差几乎就是真实误差。\n",
    "\n",
    "\n",
    "嵌套交叉验证外层有一个k折交叉验证将数据分为训练集和测试集。还有一个内部交叉验证用于选择模型算法。下图演示了一个5折外层交叉沿则和2折内部交叉验证组成的嵌套交叉验证，也被称为5*2交叉验证：\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/57727696128be.png)\n",
    "\n",
    "\n",
    "sklearn中可以如下使用嵌套交叉验证：\n",
    "\n",
    "\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/57727759c2d45.png)\n",
    "\n",
    "我们使用嵌套交叉验证比较SVm和决策树分类器：\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/06/28/577277f19857f.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## 不同的性能评价指标\n",
    "\n",
    "在前面几个章节，我们一直使用准确率(accuracy)来评价模型的性能，通常这是一个不错的选择。除此之外，还有不少评价指标哦，比如查准率(precision)、查全率(recall)和F1值(F1-score).\n",
    "\n",
    "\n",
    "### 混淆矩阵\n",
    "\n",
    "在讲解不同的评价指标之前，我们先来学习一个概念：混淆矩阵(confusion matrix), 能够展示学习算法表现的矩阵。混淆矩阵是一个平方矩阵，其中记录了一个分类器的TP(true positive)、TN(true negative)、FP(false positive)和FN(false negative):\n",
    "\n",
    "![](https://ooo.0o0.ooo/2016/07/04/577a5ca505e20.png)\n",
    "\n",
    "计算着四个指标并不复杂，不过能不手算当然就不手算啦，sklearn中提供了confusion_matrix 函数：\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    " "
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 2",
   "language": "python",
   "name": "python2"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
